﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;

namespace Tsanie.ShellExt.Definitions
{

    #region - Structs -

    enum OwnerDrawControlType : uint
    {
        Menu = 1,
        ListBox = 2,
        ComboBox = 3,
        Button = 4,
        Static = 5
    }
    enum ItemState : uint
    {
        //ODS_SELECTED = 0x0001,
        //ODS_GRAYED = 0x0002,
        //ODS_DISABLED = 0x0004,
        //ODS_CHECKED = 0x0008,
        //ODS_FOCUS = 0x0010
        Default = 256,
        Selected = 257
    }
    [StructLayout(LayoutKind.Sequential)]
    struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }
    [StructLayout(LayoutKind.Sequential)]
    struct DRAWITEMSTRUCT
    {
        public OwnerDrawControlType CtlType;
        public uint CtlID;
        public uint itemID;
        public uint itemAction;
        public ItemState itemState;
        public IntPtr hwndItem;
        public IntPtr hDC;
        public RECT rcItem;
        public IntPtr itemData;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct MEASUREITEMSTRUCT
    {
        public OwnerDrawControlType CtlType;
        public uint CtlID;
        public uint itemID;
        public uint itemWidth;
        public uint itemHeight;
        public IntPtr itemData;
    }

    #endregion

    /// <summary>
    /// ContextMenu Handler 的基类
    /// </summary>
    public abstract class ContextMenuHandlerBase : IContextMenu, IShellExtInit
    {

        #region - 私有字段 -

        FileCollection m_Files;
        List<MenuItem> m_CtxMenu = new List<MenuItem>();
        uint startCommandId;

        #region - APIs -

        const uint WM_DRAWITEM = 0x2B;
        const uint WM_MEASUREITEM = 0x2C;
        const uint WM_MENUCHAR = 0x120;

        [Flags]
        enum MenuFlags
        {
            MF_BYPOSITION = 0x400,
            MF_SEPARATOR = 0x800,
            MF_OWNERDRAW = 0x100,
            MF_POPUP = 0x10,

            MF_UNCHECKED = 0,
            MF_STRING = 0,
            MF_ENABLED = 0,
            MF_BYCOMMAND = 0,
            MF_GRAYED = 1,
            MF_DISABLED = 0x2,
            MF_CHECKED = 0x8,
            MF_HILITE = 0x80
        }
        [DllImport("user32.dll", EntryPoint = "InsertMenuA", CharSet = CharSet.Ansi)]
        extern static long InsertMenu(IntPtr hMenu, uint nPosition, MenuFlags wFlags, uint wIDNewItem, string lpNewItem);
        [DllImport("user32.dll")]
        extern static IntPtr CreatePopupMenu();
        [DllImport("user32.dll")]
        extern static int SetMenuItemBitmaps(IntPtr hmenu, uint nPosition, MenuFlags uflags, IntPtr hBitmapUnchecked, IntPtr hBitmapChecked);

        #endregion

        #endregion

        #region - 构造 -

        protected ContextMenuHandlerBase() { }

        #endregion

        #region - 公共属性 -

        /// <summary>
        /// 返回选中的文件项集合
        /// </summary>
        public FileCollection Files { get { return m_Files; } }

        /// <summary>
        /// 返回一个 MenuItemCollection
        /// </summary>
        public List<MenuItem> MenuItems { get { return m_CtxMenu; } }

        #endregion

        #region - 抽象 -

        /// <summary>
        /// 即将创建菜单时触发
        /// </summary>
        protected abstract void OnMenuCreate();

        /// <summary>
        /// 获取任务栏显示的帮助文字
        /// </summary>
        protected virtual string OnMenuSelected(MenuItem item)
        {
            return item.HelpText;
        }

        #endregion

        #region - 私有方法 -

        void AddItems(List<MenuItem> items, IntPtr hMenu, uint index, ref uint cmdId)
        {
            foreach (MenuItem menu in items)
            {
                if (menu.HasChildren)
                {
                    IntPtr popMenu = ContextMenuHandlerBase.CreatePopupMenu();
                    ContextMenuHandlerBase.InsertMenu(hMenu, index, MenuFlags.MF_BYPOSITION | MenuFlags.MF_POPUP, (uint)popMenu.ToInt32(), menu.Text);
                    AddItems(menu.MenuItems, popMenu, 0, ref cmdId);
                }
                else
                {
                    if (menu.IsSeparator)
                        ContextMenuHandlerBase.InsertMenu(hMenu, index, MenuFlags.MF_BYPOSITION | MenuFlags.MF_SEPARATOR, cmdId, menu.Text);
                    else
                    {
                        if (menu.Image != null)
                        {
                            ContextMenuHandlerBase.InsertMenu(hMenu, index, MenuFlags.MF_BYPOSITION | (menu.IsEnable ? 0 : MenuFlags.MF_DISABLED), cmdId, menu.Text); // | MenuFlags.MF_OWNERDRAW
                            IntPtr zero = menu.Image.GetHbitmap(Color.Black);
                            ContextMenuHandlerBase.SetMenuItemBitmaps(hMenu, index, MenuFlags.MF_BYPOSITION, zero, zero);
                        }
                        else
                        {
                            ContextMenuHandlerBase.InsertMenu(hMenu, index, MenuFlags.MF_BYPOSITION | (menu.IsEnable ? 0 : MenuFlags.MF_DISABLED), cmdId, menu.Text);
                        }
                    }
                    cmdId++;
                }
                index++;
            }
        }

        MenuItem FindMenu(List<MenuItem> items, int id, ref int cmdId)
        {
            foreach (MenuItem menu in items)
            {
                if (menu.HasChildren)
                {
                    MenuItem item = FindMenu(menu.MenuItems, id, ref cmdId);
                    if (item != null)
                        return item;
                }
                else
                {
                    if (cmdId == id)
                        return menu;
                    cmdId++;
                }
            }
            return null;
        }

        #endregion

        #region - IContextMenu 成员 -

        int IContextMenu.QueryContextMenu(IntPtr menuHandle, uint position, uint firstCommandId, uint lastCommandId, QueryContextMenuOptions flags)
        {
            OnMenuCreate();
            startCommandId = firstCommandId;
            // 添加菜单项到上下文菜单中
            AddItems(m_CtxMenu, menuHandle, position, ref firstCommandId);
            // 返回一共添加了多少项
#if DEBUGMENU
            Common.OutputDebugString("QueryContextMenu():: added {0} item(s).", firstCommandId - startCommandId);
#endif
            return (int)(firstCommandId - startCommandId);
        }

        void IContextMenu.InvokeCommand(ref InvokeCommandInfo invokeCommandInfo)
        {
            int cmdId = 0;
            MenuItem item = FindMenu(m_CtxMenu, invokeCommandInfo.lpVerb.ToInt32(), ref cmdId);
#if DEBUGMENU
            Common.OutputDebugString("InvokeCommand():: invoke item {0}. Text = '{1}'", invokeCommandInfo.lpVerb.ToInt32(), item.Text);
#endif
            item.PerformClick();
            m_Files.Dispose();
            m_Files = null;
            m_CtxMenu = null;
        }

        int IContextMenu.GetCommandString(IntPtr idCommand, GetCommandStringOptions flags, int reserved, IntPtr name, int cch)
        {
            if ((flags & GetCommandStringOptions.HelpText) == GetCommandStringOptions.HelpText)
            {
                // 获得帮助字符串
                int cmdId = 0;
                string text = this.OnMenuSelected(FindMenu(m_CtxMenu, idCommand.ToInt32(), ref cmdId)) + "\0";
                if (text.Length < cch)
                    cch = text.Length;
#if DEBUGMENU
                Common.OutputDebugString("GetCommandString():: item {0}. Flags = {1}, Length = {2}, Text = '{3}'.", new object[] { idCommand.ToInt32(), flags, cch, text });
#endif
                // 复制字符串到缓冲区
                if ((flags & GetCommandStringOptions.CanonicalVerb) == GetCommandStringOptions.CanonicalVerb)
                {
                    Marshal.Copy(text.ToCharArray(), 0, name, cch);
                }
                else
                {
                    Marshal.Copy(Encoding.ASCII.GetBytes(text), 0, name, cch + 1);
                }
            }
            return 0;
        }

#if IContextMenu3

        public void HandleMenuMsg(uint message, IntPtr wParam, IntPtr lParam) {
            Common.OutputDebugString("HandleMenuMsg, 0x{0:x}, wParam: {1}, lParam: 0x{2:x8}", message, wParam.ToInt32(), lParam.ToInt32());
        }

        unsafe void IContextMenu3.HandleMenuMsg2(uint message, IntPtr wParam, IntPtr lParam, IntPtr result) {
            Common.OutputDebugString("HandleMenuMsg2(uint, IntPtr, IntPtr, IntPtr):: 0x{0:x}, wParam: 0x{1:x8}, lParam: 0x{2:x8}, result: 0x{3:x8}",
                new object[] { message, wParam.ToInt32(), lParam.ToString(), result.ToInt32() }
                );
            switch (message) {
                case WM_MEASUREITEM:
                    OnMeasureItem(ref *(MEASUREITEMSTRUCT*)lParam.ToPointer());
                    break;
                case WM_DRAWITEM:
                    OnDrawItem(ref *(DRAWITEMSTRUCT*)lParam.ToPointer());
                    break;
                case WM_MENUCHAR:
                    if (result == IntPtr.Zero) {
                        Common.OutputDebugString("HandleMenuMsg2(uint, IntPtr, IntPtr, IntPtr):: WM_MENUCHAR, InvalidOperationException.");
                        return;
                    }
                    var pointerResult = (int*)result.ToPointer();
                    *pointerResult = OnMenuChar(lParam, (ushort)wParam.ToInt32());
                    break;
            }
        }
#endif

        #endregion

#if IContextMenu3
        void OnDrawItem(ref DRAWITEMSTRUCT draw) {
            Common.OutputDebugString("CtlID: {0}, CtlType: {1}, hDC: {2:x}, hwndItem: {3:x}, Action: {4}, Data: {5:x}, ID: {6}, State: {7}, rcItem: {8},{9},{10},{11}", new object[] {
                draw.CtlID,
                draw.CtlType,
                draw.hDC.ToInt32(),
                draw.hwndItem.ToInt32(),
                draw.itemAction,
                draw.itemData.ToInt32(),
                draw.itemID,
                draw.itemState,
                draw.rcItem.Left,
                draw.rcItem.Top,
                draw.rcItem.Right,
                draw.rcItem.Bottom
            });
            MenuItem item = m_CtxMenu[(int)(draw.itemID - startCommandId)];
            Graphics g = Graphics.FromHdc(draw.hDC);
            g.FillRectangle(new SolidBrush(draw.itemState == ItemState.Selected ? Color.FromKnownColor(KnownColor.Blue) : Color.FromKnownColor(KnownColor.Control)),
                (int)draw.rcItem.Left,
                (int)draw.rcItem.Top,
                (int)(draw.rcItem.Right - draw.rcItem.Left),
                (int)(draw.rcItem.Bottom - draw.rcItem.Top));
            Font font = new Font("MS UI Gothic", 9f);
            int y = (int)g.MeasureString(item.Text, font).Height;
            y = (draw.rcItem.Bottom + draw.rcItem.Top - y) / 2;
            g.DrawString(item.Text, font, new SolidBrush(Color.Black), draw.rcItem.Left + 20, y);
            g.Dispose();
        }

        void OnMeasureItem(ref MEASUREITEMSTRUCT measureItem) {
            MenuItem item = m_CtxMenu[(int)(measureItem.itemID - startCommandId)];
            Font font = new Font("MS UI Gothic", 9f);
            Graphics g = Graphics.FromImage(new Bitmap(100, 20));
            SizeF sz = g.MeasureString(item.Text, font);
            measureItem.itemHeight = (uint)sz.Height;
            measureItem.itemWidth = (uint)sz.Width;
            g.Dispose();
        }

        int OnMenuChar(IntPtr handleMenu, ushort character) {
#if TRACE
            Common.OutputDebugString("OnMenuChar(IntPtr, ushort):: 0x{0:x}, 0x{1:x4} '{2}'", handleMenu.ToInt32(), character, (char)character);
#endif
            int result = 768;   // 3 << 16  (MenuCharResultSelect)
            for (int i = 0; i < m_CtxMenu.Count; i++) {
                if (i == 0)
                    return result | (int)(i + startCommandId);
            }
            return 0;   // MenuCharResultIgnore
        }
#endif

        #region - IShellExtInit 成员 -

        void IShellExtInit.Initialize(IntPtr pidlFolder, IDataObject lpIDataObject, uint hkeyProgID)
        {
            m_Files = new FileCollection(lpIDataObject);
        }

        #endregion

        #region - COM 注册 -

        [System.Runtime.InteropServices.ComRegisterFunction()]
        static void ComReg(Type t)
        {
            Common.RegisterExtension(t, "ContextMenuHandlers\\" + t.FullName);
        }

        [System.Runtime.InteropServices.ComUnregisterFunction()]
        static void ComUnreg(Type t)
        {
            Common.UnregisterExtension(t, "ContextMenuHandlers\\" + t.FullName);
        }

        #endregion

    }

    #region - 菜单项类 -

    /// <summary>
    /// 菜单项类
    /// </summary>
    public class MenuItem
    {
        /// <summary>
        /// 获取或设置菜单显示文字
        /// </summary>
        public string Text { get; set; }
        /// <summary>
        /// 获取或设置菜单帮助文字
        /// </summary>
        public string HelpText { get; set; }
        /// <summary>
        /// 获取或设置菜单图标
        /// </summary>
        public Bitmap Image { get; set; }
        /// <summary>
        /// 获取或设置是否是分隔符
        /// </summary>
        public bool IsSeparator { get; set; }
        /// <summary>
        /// 获取或设置是否可用
        /// </summary>
        public bool IsEnable { get; set; }
        /// <summary>
        /// 获取或设置子菜单项
        /// </summary>
        public List<MenuItem> MenuItems { get { return m_MenuItems; } }
        private List<MenuItem> m_MenuItems;
        /// <summary>
        /// 当菜单单击时触发
        /// </summary>
        public EventHandler OnClick { get; set; }
        /// <summary>
        /// 获取该菜单是否有子菜单
        /// </summary>
        public bool HasChildren { get { return (m_MenuItems != null && m_MenuItems.Count > 0); } }

        public MenuItem()
        {
            this.IsEnable = true;
            this.Text = string.Empty;
            this.HelpText = string.Empty;
            m_MenuItems = new List<MenuItem>();
        }

        /// <summary>
        /// 手动触发该菜单
        /// </summary>
        public void PerformClick()
        {
            if (OnClick != null)
                OnClick(this, EventArgs.Empty);
        }
    }

    #endregion

}
